﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Animation;

namespace XDCCManager.Controls
{
	/// <summary>
	/// Interaction logic for LoadingAnimation.xaml
	/// </summary>
	public partial class LoadingAnimation : UserControl, IAnimationStartStop
	{
		private Storyboard animation;

		public LoadingAnimation()
		{
			AnimationImage = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Bgra32, null);
			this.animation = new Storyboard() { RepeatBehavior = RepeatBehavior.Forever };
			DoubleAnimationUsingKeyFrames animation = new DoubleAnimationUsingKeyFrames();
			animation.KeyFrames.Add(new EasingDoubleKeyFrame(0.0, KeyTime.FromTimeSpan(TimeSpan.Zero)));
			animation.KeyFrames.Add(new EasingDoubleKeyFrame(360.0, KeyTime.FromTimeSpan(new TimeSpan(0, 0, 0, 1, 500))));
			Storyboard.SetTargetProperty(animation, new PropertyPath(LoadingAnimation.AnimationAngleProperty));
			Storyboard.SetTarget(animation, this);
			this.animation.Children.Add(animation);
			this.InitializeComponent();
			DrawImage();
		}

		public WriteableBitmap AnimationImage
		{
			get { return (WriteableBitmap)GetValue(AnimationImageProperty); }
			set { SetValue(AnimationImageProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationImage.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation image object.
		/// </summary>
		public static readonly DependencyProperty AnimationImageProperty =
			DependencyProperty.Register("AnimationImage", typeof(WriteableBitmap), typeof(LoadingAnimation), new UIPropertyMetadata(null, null, null, true));

		public Size AnimationSize
		{
			get { return (Size)GetValue(AnimationSizeProperty); }
			set { SetValue(AnimationSizeProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationSize.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation image size.
		/// </summary>
		public static readonly DependencyProperty AnimationSizeProperty =
			DependencyProperty.Register("AnimationSize", typeof(Size), typeof(LoadingAnimation), new UIPropertyMetadata(new Size(32, 32), UpdateBitmap));

		public double AnimationAngle
		{
			get { return (double)GetValue(AnimationAngleProperty); }
			set { SetValue(AnimationAngleProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationAngle.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation drawing starting angle.
		/// The angle at which the foreground color is drawn in the progress ring.
		/// </summary>
		public static readonly DependencyProperty AnimationAngleProperty =
			DependencyProperty.Register("AnimationAngle", typeof(double), typeof(LoadingAnimation), new UIPropertyMetadata(0.0, UpdateAnimation));

		public double AnimationAngleSpan
		{
			get { return (double)GetValue(AnimationAngleSpanProperty); }
			set { SetValue(AnimationAngleSpanProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationAngleSpan.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation drawing arc length.
		/// The angle span during which the color goes from foreground to background in the progress ring.
		/// </summary>
		public static readonly DependencyProperty AnimationAngleSpanProperty =
			DependencyProperty.Register("AnimationAngleSpan", typeof(double), typeof(LoadingAnimation), new UIPropertyMetadata(270.0));

		public Color AnimationForeground
		{
			get { return (Color)GetValue(AnimationForegroundProperty); }
			set { SetValue(AnimationForegroundProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationForeground.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation progress ring foreground color.
		/// </summary>
		public static readonly DependencyProperty AnimationForegroundProperty =
			DependencyProperty.Register("AnimationForeground", typeof(Color), typeof(LoadingAnimation), new UIPropertyMetadata(Colors.Black, UpdateAnimation));

		public Color AnimationBackground
		{
			get { return (Color)GetValue(AnimationBackgroundProperty); }
			set { SetValue(AnimationBackgroundProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationBackground.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation progress ring background color.
		/// </summary>
		public static readonly DependencyProperty AnimationBackgroundProperty =
			DependencyProperty.Register("AnimationBackground", typeof(Color), typeof(LoadingAnimation), new UIPropertyMetadata(Colors.White, UpdateAnimation));

		public Color AnimationOutside
		{
			get { return (Color)GetValue(AnimationOutsideProperty); }
			set { SetValue(AnimationOutsideProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationOutside.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation background color. Outside of the ring.
		/// </summary>
		public static readonly DependencyProperty AnimationOutsideProperty =
			DependencyProperty.Register("AnimationOutside", typeof(Color), typeof(LoadingAnimation), new UIPropertyMetadata(Colors.Transparent, UpdateAnimation));

		public double AnimationRingWidth
		{
			get { return (double)GetValue(AnimationRingWidthProperty); }
			set { SetValue(AnimationRingWidthProperty, value); }
		}

		// Using a DependencyProperty as the backing store for AnimationRingWidth.  This enables animation, styling, binding, etc...
		/// <summary>
		/// The loading animation progress ring width.
		/// </summary>
		public static readonly DependencyProperty AnimationRingWidthProperty =
			DependencyProperty.Register("AnimationRingWidth", typeof(double), typeof(LoadingAnimation), new UIPropertyMetadata(4.0, UpdateAnimation));

		private static void UpdateBitmap(DependencyObject @do, DependencyPropertyChangedEventArgs args)
		{
			LoadingAnimation cp = @do as LoadingAnimation;
			if (cp == null)
				return;
			cp.AnimationImage = new WriteableBitmap((int)Math.Ceiling(cp.AnimationSize.Width), (int)Math.Ceiling(cp.AnimationSize.Width), 96, 96, PixelFormats.Bgra32, null);
			cp.DrawImage();
		}

		private static void UpdateAnimation(DependencyObject @do, DependencyPropertyChangedEventArgs args)
		{
			LoadingAnimation cp = @do as LoadingAnimation;
			if (cp == null)
				return;
			cp.DrawImage();
		}

		private void DrawImage()
		{
			HslColor start = AnimationForeground.ToHslColor();
			HslColor end = AnimationBackground.ToHslColor();
			HslColor outside = AnimationOutside.ToHslColor();
			Color startColor = AnimationForeground;
			Color endColor = AnimationBackground;
			Color outsideColor = AnimationOutside;
			double startAngle = NormalizeAngle(AnimationAngle);
			bool spanDirectionClockWise = AnimationAngleSpan < 0;
			double endAngle = NormalizeAngle(startAngle - AnimationAngleSpan);
			int width = AnimationImage.PixelWidth;
			int height = AnimationImage.PixelHeight;
			double centerX = width / 2.0;
			double centerY = height / 2.0;
			double outerRadius = Math.Min(AnimationSize.Height, AnimationSize.Width) / 2;
			double innerRadius = outerRadius - Math.Max(0, AnimationRingWidth);
			AnimationImage.Lock();
			IntPtr bufferLocation = AnimationImage.BackBuffer;
			int stride = AnimationImage.BackBufferStride;
			for (int y = 0; y < height; y++)
			{
				int rowLocation = bufferLocation.ToInt32() + stride * y;
				for (int x = 0; x < width; x++)
				{
					int pixelLocation = rowLocation + x * 4;
					Point coords = new Point(x - centerX, y - centerY);
					double radius = Math.Sqrt(coords.X * coords.X + coords.Y * coords.Y);
					if (((radius - .5) > outerRadius) || ((radius + .5) < innerRadius))
					{
						unsafe
						{
							*((int*)pixelLocation) = ColorValue(outsideColor);
						}
						continue;
					}
					double teta = NormalizeAngle(ComputeAngle(coords.X, coords.Y) * 180 / Math.PI);
					HslColor angleColor = end;
					double angleRatio = ComputeAngleRatio(startAngle, endAngle, spanDirectionClockWise, teta);
					if (angleRatio > 0)
					{
						angleColor = CombineColors(start, end, angleRatio);
					}
					else if (Math.Round(teta - startAngle) == 0)
					{
						angleColor = CombineColors(start, end, (spanDirectionClockWise) ? (teta - startAngle + .5) : (.5 + startAngle - teta));
					}
					HslColor finalColor = angleColor;
					if (Math.Round(radius - outerRadius) == 0)
					{
						finalColor = CombineColors(finalColor, outside, .5 + outerRadius - radius);
					}
					if (Math.Round(radius - innerRadius) == 0)
					{
						finalColor = CombineColors(finalColor, outside, .5 - outerRadius + radius);
					}
					unsafe
					{
						*((int*)pixelLocation) = ColorValue(finalColor);
					}
				}
			}
			AnimationImage.AddDirtyRect(new Int32Rect(0, 0, width, height));
			AnimationImage.Unlock();
		}

		private double ComputeAngleRatio(double startAngle, double endAngle, bool spanDirectionClockWise, double teta)
		{
			double angleRatio = 0;
			if (spanDirectionClockWise)
			{
				if (startAngle < endAngle)
				{
					if ((teta >= startAngle) && (teta < endAngle))
						angleRatio = (endAngle - teta) / (endAngle - startAngle);
				}
				else
				{
					if ((teta >= startAngle) || (teta < endAngle))
						angleRatio = NormalizeAngle(360 + endAngle - teta) / (360 + endAngle - startAngle);
				}
			}
			else
			{
				if (startAngle > endAngle)
				{
					if ((teta > endAngle) && (teta <= startAngle))
						angleRatio = (teta - endAngle) / (startAngle - endAngle);
				}
				else
				{
					if ((teta > endAngle) || (teta <= startAngle))
						angleRatio = NormalizeAngle(360 - endAngle + teta) / (360 - endAngle + startAngle);
				}
			}
			return angleRatio;
		}

		private static double ComputeAngle(double x, double y)
		{
			if (x == 0)
			{
				if (y == 0)
					return 0;
				else if (y < 0)
					return -Math.PI / 2;
				else
					return Math.PI / 2;
			}
			else if (x < 0)
			{
				if (y < 0)
					return Math.Atan(y / x) - Math.PI;
				else
					return Math.Atan(y / x) + Math.PI;
			}
			else
				return Math.Atan(y / x);
		}

		private double NormalizeAngle(double angle)
		{
			while (angle < 0) angle += 360; while (angle >= 360) angle -= 360;
			return angle;
		}

		private HslColor CombineColors(HslColor first, HslColor second, double ratio)
		{
			ratio = Math.Max(0, Math.Min(1, ratio));
			int ratioNorm = (int)Math.Round(ratio * 100);
			int compRatioNorm = 100 - ratioNorm;
			return HslColor.FromHsla(
				(first.Hue * ratioNorm + second.Hue * compRatioNorm) / 100,
				(first.Saturation * ratioNorm + second.Saturation * compRatioNorm) / 100,
				(first.Lightness * ratioNorm + second.Lightness * compRatioNorm) / 100,
				(byte)((first.Alpha * ratioNorm + second.Alpha * compRatioNorm) / 100)
			);
		}

		private int ColorValue(Color c)
		{
			return c.A * 0x1000000 + c.R * 0x10000 + c.G * 0x100 + c.B;
		}

		private int ColorValue(HslColor hslC)
		{
			Color c = hslC.ToColor();
			return c.A * 0x1000000 + c.R * 0x10000 + c.G * 0x100 + c.B;
		}

		#region IAnimationStartStop Members

		public void Begin()
		{
			animation.Begin();
		}

		public void End()
		{
			animation.Stop();
		}

		#endregion
	}
}